// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.SpatialProperties;
import bats.missile.Shafts;
import com.jme3.app.Application;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionListener;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import java.util.logging.Logger;
import jme3utilities.physics.OverlapListener;

/**
 * A world component to manage physics, including physics spaces, collision
 * event dispatching, and debug visualizations.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Physics
        implements PhysicsCollisionListener {
    // *************************************************************************
    // constants

    /**
     * Continuous Collision Detection motion threshold for rigid body physics
     * (in meters)
     */
    final public static float ccdMotionThreshold = 1e-3f;
    /**
     * magnitude of downward acceleration due to gravity (in meters/second^2)
     */
    final public static float gravityMagnitude = 9.81f;
    /**
     * physics tick frequency (per second)
     */
    final public static float physicsHertz = 100f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Physics.class.getName());
    // *************************************************************************
    // fields
    /**
     * application state for the Bullet physics engine: set by constructor (not
     * null)
     */
    final private BulletAppState bulletAppState;
    /**
     * scene in which the objects reside: set by constructor (not null)
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate (paused) physics for a specified scene.
     *
     * @param scene which scene (not null)
     */
    Physics(SceneNode scene) {
        assert scene != null;

        this.scene = scene;
        /*
         * Initialize the Bullet Physics appState for the world.  Use parallel
         * threading so that objects in different worlds won't collide.
         */
        bulletAppState = new BulletAppState();
        bulletAppState.setSpeed(0f);
        bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);

        Application bats = scene.getApplication();
        bats.getStateManager().attach(bulletAppState);
        /*
         * Initialize the physics space for the world.
         */
        PhysicsSpace space = bulletAppState.getPhysicsSpace();
        space.setAccuracy(1f / physicsHertz);
        float worldGravity = gravityMagnitude / scene.getScaleFactor();
        Vector3f gravityVector = new Vector3f(0f, -worldGravity, 0f);
        space.setGravity(gravityVector);
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Add a collision object to the physics space.
     *
     * @param object (not null)
     */
    public void addObject(PhysicsCollisionObject object) {
        assert object != null;
        getSpace().add(object);
    }

    /**
     * Access the world's physics space.
     *
     * @return the pre-existing instance (not null)
     */
    public PhysicsSpace getSpace() {
        PhysicsSpace physicsSpace = bulletAppState.getPhysicsSpace();
        assert physicsSpace != null;
        return physicsSpace;
    }

    /**
     * Test whether debug visualizations are enabled.
     *
     * @return true if enabled (visible), false if disabled (hidden)
     */
    public boolean isDebugging() {
        return bulletAppState.isDebugEnabled();
    }

    /**
     * Process an action which has just begun.
     *
     * @param words action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        String verb = words[0];
        switch (verb) {
            case "toggle":
                if (words.length == 2) {
                    String what = words[1];
                    switch (what) {
                        case "physics":
                            /*
                             * Toggle physics debug rendering.
                             */
                            toggleDebugging();
                            return true;
                    }
                }
                break;
        }
        return false;
    }

    /**
     * Enable or disable debug visualizations.
     *
     * @param newState true to enable/reveal, false to disable/hide
     */
    public void setDebugging(boolean newState) {
        bulletAppState.setDebugEnabled(newState);
    }

    /**
     * Alter the effective simulation rate.
     *
     * @param newSpeed simulation rate (paused=0, standard rate=1)
     */
    public void setSpeed(float newSpeed) {
        if (getSpeed() == newSpeed) {
            return;
        }
        bulletAppState.setSpeed(newSpeed);
        if (newSpeed == 0f) {
            getSpace().removeCollisionListener(this);
        } else {
            getSpace().addCollisionListener(this);
        }
    }
    // *************************************************************************
    // PhysicsCollisionListener methods

    /**
     * React to a collision event from the physics engine.
     *
     * @param event details of the collision (not null)
     */
    @Override
    public void collision(PhysicsCollisionEvent event) {
        assert scene.isAttached();
        if (!scene.isEnabled()) {
            return;
        }

        Spatial aSpatial = event.getNodeA();
        if (aSpatial == null) {
            return;
        }
        assert scene.contains(aSpatial) : aSpatial;

        Spatial bSpatial = event.getNodeB();
        if (bSpatial == null) {
            return;
        }
        assert scene.contains(bSpatial) : bSpatial;
        /*
         * Ignore self-collisions.
         */
        if (aSpatial == bSpatial) {
            return;
        }
        /*
         * Ignore any collision between a missile and its launcher.
         */
        if (Shafts.isShaft(aSpatial)
                && SpatialProperties.isLauncher(bSpatial, aSpatial)) {
            return;
        }
        if (Shafts.isShaft(bSpatial)
                && SpatialProperties.isLauncher(aSpatial, bSpatial)) {
            return;
        }
        /*
         * Invoke the onOverlap() method on each overlapping object which
         * implements that method, providing the other object is a rigid body.
         */
        PhysicsCollisionObject aObject = event.getObjectA();
        assert aObject != null;
        PhysicsCollisionObject bObject = event.getObjectB();
        assert bObject != null;
        if (aObject instanceof OverlapListener
                && bObject instanceof PhysicsRigidBody) {
            PhysicsRigidBody bBody = (PhysicsRigidBody) bObject;
            Vector3f localPoint = event.getLocalPointA();
            OverlapListener listener = (OverlapListener) aObject;
            listener.onOverlap(bBody, bSpatial, localPoint);
        }
        if (bObject instanceof OverlapListener
                && aObject instanceof PhysicsRigidBody) {
            PhysicsRigidBody aBody = (PhysicsRigidBody) aObject;
            Vector3f localPoint = event.getLocalPointB();
            OverlapListener listener = (OverlapListener) bObject;
            listener.onOverlap(aBody, aSpatial, localPoint);
        }
    }
    // *************************************************************************
    // private methods

    /**
     * Read the effective simulation rate.
     */
    private float getSpeed() {
        float result = bulletAppState.getSpeed();
        return result;
    }

    /**
     * Toggle the debug visualization state.
     */
    private void toggleDebugging() {
        boolean debugFlag = isDebugging();
        setDebugging(!debugFlag);
    }
}